package top.wenzz.commons.service;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.BeansException;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
import org.springframework.data.domain.Page;
import org.springframework.data.jpa.domain.Specification;
import org.springframework.transaction.annotation.Transactional;
import top.wenzz.commons.annotation.Edit;
import top.wenzz.commons.annotation.Query;
import top.wenzz.commons.dao.BasicDao;
import top.wenzz.commons.entity.DataEntity;
import top.wenzz.commons.entity.LoginUser;
import top.wenzz.commons.utils.SpringSecurityUtils;
import top.wenzz.commons.utils.StringUtils;
import top.wenzz.commons.vo.PageRequest;

import javax.persistence.criteria.*;
import java.lang.annotation.Annotation;
import java.lang.reflect.AccessibleObject;
import java.lang.reflect.Array;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.sql.Timestamp;
import java.util.*;

/**
 * 基础的service
 * Created by 钟廷员 on 2016/12/6.
 */
@Transactional(readOnly = true)
public class BasicService<E extends DataEntity, D extends BasicDao> implements ApplicationContextAware {

    ApplicationContext applicationContext;

    Logger logger = LoggerFactory.getLogger(this.getClass());
    private static final Map<String, Map<String, Field>> fieldMap = new HashMap<>();

    @Autowired
    protected D dao;

    @Transactional
    protected void saveEntity(E entity) {
        Timestamp time = new Timestamp(System.currentTimeMillis());
        LoginUser loginUser = SpringSecurityUtils.getUserByContext();
        if(loginUser != null) {
            entity.setCreateUserId(String.valueOf(loginUser.getId()));
            entity.setUpdateUserId(String.valueOf(loginUser.getId()));
        }
        entity.setCreateTime(time);
        entity.setUpdateTime(time);

        dao.save(entity);
    }

    @Transactional
    public String save(E entity) throws Exception {
        E main = (E) entity.getClass().newInstance();
        setEntity(main, entity, 1);
        saveEntity(main);
        return null;
    }

    @Transactional
    protected void updateEntity(E entity) throws Exception {
        Timestamp time = new Timestamp(System.currentTimeMillis());
        entity.setUpdateTime(time);
        entity.setUpdateUserId(String.valueOf(SpringSecurityUtils.getUserByContext().getId()));
        dao.save(entity);
    }

    @Transactional
    public String update(E entity) throws Exception {
        E e = getOne(entity.getId());
        if(e == null)
            return "不存在该数据";
        if(e.getVersion() != entity.getVersion())
            return "数据已被修改，请刷新后操作";
        setEntity(e, entity, 2);
        updateEntity(e);
        return null;
    }

    @Transactional
    public String delete(int id, int version) throws Exception {
        E entity = getOne(id);
        if(entity == null) {
            return "不存在该数据";
        }
        if(version != entity.getVersion())
            return "数据已被修改，请刷新后操作";
        dao.delete(entity);
        return null;
    }

    protected void setEntity(E main, E copy, int cu) throws Exception {
        Class<?> clazz = copy.getClass();
        Field[] fields = clazz.getDeclaredFields();
        for(Field field : fields) {
            handleEntity(main, copy, cu, field);
        }
        Method[] methods = clazz.getDeclaredMethods();
        for(Method method : methods) {
            handleEntity(main, copy, cu, method);
        }
    }

    private void handleEntity(E main, E copy, int cu, AccessibleObject obj) throws Exception {
        if (!obj.isAnnotationPresent(Edit.class)) {
            return;
        }
        Annotation annotation = obj.getAnnotation(Edit.class);
        String name = (String) annotation.getClass().getDeclaredMethod("value").invoke(annotation);
        boolean create = (boolean) annotation.getClass().getDeclaredMethod("create").invoke(annotation);
        boolean update = (boolean) annotation.getClass().getDeclaredMethod("update").invoke(annotation);
        // Class<?> serviceClass = (Class) annotation.getClass().getDeclaredMethod("service").invoke(annotation);

        if(!((cu == 1 && create) || (cu == 2 && update)))
            return;

        AccessibleObjectName value = getAccessibleObjectName(obj, copy);
        if(value.value == null)
            return;

        /*if(DataEntity.class.isAssignableFrom(value.pType)) {
            DataEntity entity = (DataEntity) value.value;
            BasicService service = (BasicService) applicationContext.getBean(serviceClass);
            value.value = service.getOne(entity.getId());
            if(value.value == null)
                throw new NotFindEntityException(name.concat("不存在"));
        }*/

        value.field.set(main, value.value);
    }

    //---------------------------------------查询------------------------------------------------//
    public List<E> findAll() {
        return dao.findAll();
    }

    protected Page findAll(org.springframework.data.domain.PageRequest pageRequest) {
        return dao.findAll(pageRequest);
    }

    protected Page findAll(Specification<E> specification, org.springframework.data.domain.PageRequest pageRequest) {
        return dao.findAll(specification, pageRequest);
    }

    public Map<String, Object> find(PageRequest pageRequest) {
        return toExtjsQuery(this.findAll(pageRequest.init()), pageRequest);
    }

    public Map<String, Object> find(E entity, PageRequest pageRequest) {
        Page page = this.findAll(getSpecification(entity), pageRequest.init());
        return toExtjsQuery(page, pageRequest);
    }

    public List<E> find(E entity) {
        return dao.findAll(getSpecification(entity));
    }

    private Specification<E> getSpecification(E entity) {
        Specification<E> sf = (root, criteriaQuery, criteriaBuilder) -> {
            List<Predicate> predicates = new ArrayList<>();
            getPredicate(root, criteriaBuilder, entity, predicates, null);
            return criteriaBuilder.and(predicates.toArray(new Predicate[predicates.size()]));
        };
        return sf;
    }

    private void getPredicate(Root<E> root, CriteriaBuilder criteriaBuilder, DataEntity entity, List<Predicate> predicates, String parentName) {
        Set<AccessibleObject> accessibleObjects = new HashSet<>();
        accessibleObjects.addAll(Arrays.asList(entity.getClass().getDeclaredMethods()));
        accessibleObjects.addAll(Arrays.asList(entity.getClass().getSuperclass().getDeclaredMethods()));
        accessibleObjects.addAll(Arrays.asList(entity.getClass().getSuperclass().getSuperclass().getDeclaredMethods()));
        accessibleObjects.addAll(Arrays.asList(entity.getClass().getDeclaredFields()));
        accessibleObjects.addAll(Arrays.asList(entity.getClass().getSuperclass().getDeclaredFields()));
        accessibleObjects.addAll(Arrays.asList(entity.getClass().getSuperclass().getSuperclass().getDeclaredFields()));

        accessibleObjects.forEach(method -> {
            try {
                if (!method.isAnnotationPresent(Query.class))
                    return;
                Annotation annotation = method.getAnnotation(Query.class);
                String queryName = (String) annotation.getClass().getDeclaredMethod("queryName").invoke(annotation);
                Query.ReadType readType = (Query.ReadType) annotation.getClass().getDeclaredMethod("value").invoke(annotation);

                AccessibleObjectName aon = getAccessibleObjectName(method, entity);
                // 数据为空 or 字段是id=0 不进行查询
                if(StringUtils.isEmpty(aon.value) || ("id".equals(aon.pName) && (int) aon.value == 0))
                    return;

                if(StringUtils.isEmpty(queryName))
                    queryName = aon.pName;

                queryName = StringUtils.isNotEmpty(parentName) ? parentName.concat(".").concat(queryName) : queryName;
                if(DataEntity.class.isAssignableFrom(aon.pType)) {
                    getPredicate(root, criteriaBuilder, (DataEntity) aon.value, predicates, queryName);
                    return;
                }

                switch (readType) {
                    case EQ:
                        predicates.add(criteriaBuilder.equal(getPath(root, queryName), aon.value));
                        break;
                    case LIKE:
                        predicates.add(criteriaBuilder.like(getPath(root, queryName), "%".concat(aon.value.toString()).concat("%")));
                        break;
                    case LLIKE:
                        predicates.add(criteriaBuilder.like(getPath(root, queryName), "%".concat(aon.value.toString())));
                        break;
                    case RLIKE:
                        predicates.add(criteriaBuilder.like(getPath(root, queryName), aon.value.toString().concat("%")));
                        break;
                    case GT:
                        predicates.add(criteriaBuilder.gt(getPath(root, queryName), StringUtils.formatNumber(aon.value)));
                        break;
                    case LT:
                        predicates.add(criteriaBuilder.gt(getPath(root, queryName), StringUtils.formatNumber(aon.value)));
                        break;
                    case LE:
                        predicates.add(criteriaBuilder.le(getPath(root, queryName), StringUtils.formatNumber(aon.value)));
                    case GE:
                        predicates.add(criteriaBuilder.ge(getPath(root, queryName), StringUtils.formatNumber(aon.value)));
                        break;
                    case IN:
                        int length = Array.getLength(Array.getLength(aon.value));
                        Object[] ins = new Object[length];
                        for (int k = 0; k < length; k++) {
                            ins[k] = Array.get(aon.value, k);
                        }
                        predicates.add(root.get(queryName).in(ins));
                        break;
                    case BETWEEN:
                        List<Date> list;
                        Date[] array;
                        Date start = null;
                        Date end = null;
                        if(aon.value instanceof List && (list = (List) aon.value).size() == 2) {
                            start = list.get(0);
                            end = list.get(1);
                        } else if(aon.value instanceof Date[] && (array = (Date[]) aon.value).length == 2) {
                            start = array[0];
                            end = array[1];
                        }
                        if(start != null || end != null) {
                            start = start == null ? new Date(System.currentTimeMillis() - 3153600000000L) : start;
                            end = end == null ? new Date(System.currentTimeMillis() + 3153600000000L) : end;
                            predicates.add(criteriaBuilder.between(getPath(root, queryName), start, end));
                        }
                        break;
                }
            } catch (Exception e) {
                logger.error("查询异常", e);
            }
        });
    }

    private Map<String, Object> toExtjsQuery(Page page, PageRequest pageRequest) {
        return toExtjsQuery(page.getContent(), page.getTotalElements(), pageRequest);
    }

    protected Map<String, Object> toExtjsQuery(Object rows, long total, PageRequest pageRequest) {
        Map<String, Object> map = new HashMap<>();
        map.put("rows", rows);
        if(pageRequest != null) {
            pageRequest.setTotal(total);
            map.put("pagination", pageRequest);
        } else {
            map.put("pagination", new PageRequest());
        }
        return map;
    }

    public E getOne(int id) {
        try {
            return (E) dao.findOne(id);
        } catch (Exception e) {
            logger.info("查询ID：{}，异常：{}", id, e);
            return null;
        }
    }

    //---------------------------工具--------------------------------//
    private AccessibleObjectName getAccessibleObjectName(AccessibleObject obj, DataEntity entity) throws IllegalAccessException {
        Map<String, Field> fieldMap = getFieldMap(entity.getClass());
        String pName;
        Object value;
        Class pType;

        Field field;
        if(obj instanceof Field) {
            field = (Field) obj;
            field.setAccessible(true);
            value = field.get(entity);
            pType = field.getType();
            pName = field.getName();
            return new AccessibleObjectName(pName, value, pType, field);
        }
        Method method = (Method) obj;
        if(method.getParameters().length == 1) {
            pType = method.getParameters()[0].getType();
            pName = method.getParameters()[0].getName();
        } else if(method.getReturnType().getName().equals("boolean")) {
            pType = method.getReturnType();
            pName = StringUtils.firstLetterLowerCase(method.getName().substring(2));
        } else {
            pType = method.getReturnType();
            pName = StringUtils.firstLetterLowerCase(method.getName().substring(3));
        }
        field = fieldMap.get(pName);
        field.setAccessible(true);
        value = field.get(entity);
        return new AccessibleObjectName(pName, value, pType, field);
    }

    @Override
    public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
        this.applicationContext = applicationContext;
    }

    private static class AccessibleObjectName {
        String pName;
        Object value;
        Class pType;
        Field field;

        AccessibleObjectName(String pName, Object value, Class pType, Field field) {
            this.pName = pName;
            this.value = value;
            this.pType = pType;
            this.field = field;
        }
    }

    private static Map<String, Field> getFieldMap(Class<?> clazz) {
        if(fieldMap.containsKey(clazz.getName())) {
            return fieldMap.get(clazz.getName());
        }
        synchronized ("getFieldMap") {
            if(fieldMap.containsKey(clazz.getName())) {
                return fieldMap.get(clazz.getName());
            }
            Map<String, Field> fm = new HashMap<>();
            Arrays.stream(clazz.getDeclaredFields()).forEach(f -> fm.put(f.getName(), f));
            Arrays.stream(clazz.getSuperclass().getDeclaredFields()).forEach(f -> fm.put(f.getName(), f));
            Arrays.stream(clazz.getSuperclass().getSuperclass().getDeclaredFields()).forEach(f -> fm.put(f.getName(), f));
            fieldMap.put(clazz.getName(), fm);
            return fm;
        }
    }

    private Path getPath(Root root, String readName) {
        Path path = null;
        for(String name : readName.split("\\.")) {
            if(path == null)
                path = root.get(name);
            else
                path = path.get(name);
        }
        return path;
    }
}
